Arc Post-Mortem Summary.
This arc refreshed the one-to-one chat. It moved message delivery onto a
plain-text push channel and preferred WebSockets over server-sent events
(I), and rebuilt the input row into a single rounded pill: a plus button
opens the attachment dialog, the emoji button and an auto-growing textarea
sit in the pill, and the trailing control swaps between a microphone and a
send arrow as text is entered (II). It removed the per-conversation
translation feature and its per-message API rather than reworking them (IV),
which left nothing tertiary in the header, so the planned overflow menu was
dropped (III); the message bubbles were recolored through the Phase 5 custom
properties, the sender's own tinted and the other party's neutral (V).
Alongside the planned work it added smart-HTTP serving so the repository's
wiki pages can be git-cloned, per-domain favicon serving, and it centered the
header's video-call symbol after finding that a setDisplay call
was overriding the button's flex layout with block.
Arc started 2026-07-21, after Phase 5. Sections are Roman numerals and steps within them are lettered; each step's title says what it does, followed by a sentence on what was proposed and, once worked, a few on what was done and anything learned. Marker legend: ▶ being worked on, ⚑ implemented and waiting to be reviewed, ✓ done, ✓✗ closed by deciding not to do it, ? open decision, unmarked = planned.
Yioop's group messaging works but shows its age. The message that carries a chat still arrives by polling in the common case: plain text messaging asks the server for new messages on a timer, and the server's push channel is only opened while a voice or video call is up. The input row is a run of separate controls — a paper-clip link, an emoji button, a single-line text field, and a microphone button sitting beside each other — rather than the single rounded field a person expects from a modern chat. The header carries every action inline, the per-conversation translation control reads as clutter, and the message bubbles do not yet use the color and spacing groundwork Phase 5 laid down. This arc brings the chat up to date on both counts: how a message arrives (push instead of poll, where the server can push), and how the chat looks and feels to use. It builds on Phase 5's custom properties for the visual work and on atto's WebSocket support for the delivery work.
USER_MESSAGE_STATUS_POLL_TIME. The full push,
without any reconnect, ships in I.B. To-do item 2. Done./ws-messages WebSocket route
holds a conversation's socket open and pushes each new message the moment
it lands; browsers on a server that cannot hold a socket keep using the
event stream. This needed one general addition to the atto server: a
WebSocket route now sees its handshake request through the usual request
globals, so it can authenticate the connection. To-do item 1. Done.⊕ (🙂 New message 🎤): a filled
⊕ circle sits to the left of the pill and opens the
attachment dialog directly, and the pill (#user-input-container,
a --gray-93 surface with a --gray-80 border)
holds the emoji button, the growing text field, and the trailing
record/send control. Keeping the emoji and attach as their own one-tap
controls avoids a nested "emoji or attachment" chooser. In
UsermessagesElement the file input and its now-plus attach
left the pill's left edge to become that circle, and the record, audio,
and send controls moved inside the pill; in messages.css the
attach became the round circle, the emoji, mic, and send became
transparent inline icons, and the text field grows to fill
(flex: 1; min-width: 0). Rendered headless at 360px and
800px with no viewport overflow. To-do item 6.<input type="text">
became a <textarea> whose height is set to its own
scrollHeight on each input by
autoGrowMessageField, so it starts one line tall and grows
with the text up to a 6rem cap, after which it scrolls; the
pill grows with it and bottom-aligns its controls. A keydown handler
sends on Enter (through the form's requestSubmit, only when
the field is not empty) and lets Shift+Enter start a new line. Rendered
headless at one, three, and many lines with no viewport overflow and the
height capping as intended. To-do item 6.input event drives updateMessageSendControl,
which shows the microphone (record) while the field is empty and the send
button once anything is typed. So the recorded-audio case also flips to
send and back, initializeAudioRecorder now fires an
input event when it fills the field on stop and when it
clears the field on delete, and the two direct
record_button.style.display writes it used to make, now
redundant, were removed. To-do item 6.SocialComponent translation methods
(setTranslationLanguage, translateMessageBatch,
getTranslationLocales, translateText) and the
two message-path blocks that translated a loaded batch; the
GroupModel per-contact translation storage
(get/setTranslationLanguage); the
ApiController translate action and its
translateText, with translate dropped from that
controller's activity list; and the translation-language CSS. The general
localization system (tl(), locale files) is untouched. To-do
item 8.back-light-blue/back-light-gray
utilities to semantic message-mine/message-theirs
classes, set in both the initial render (UsermessagesElement)
and the refresh append (SocialComponent). The sender's own
messages now take a tinted bubble (--color-topbar) and the
other party's a neutral one (--gray-93), each with a
--gray-80 border for definition, so both track light and
dark. Time labels between message groups are unchanged. The quoted-reply
visual has no target yet — the chat has no reply/quote feature to
style — so that part waits until one exists. The light-mode tint is
deliberately subtle (the pale custom properties); say if you want more
contrast between the two sides. To-do item 9.The delivery work (I) and the visual work (II–V) are independent and can proceed in either order. Within delivery, push plain messaging (I.A) comes before choosing the channel (I.B), since the channel choice is a refinement of the push path. Within the input row, the pill (II.A) comes before the growing field and the mic/send swap (II.B, II.C) sit inside it. The translation decision (IV) is an open question to settle with Chris before it is worked, so it is marked with a question rather than planned as written. The bubble polish (V) depends only on the Phase 5 custom properties, which are in place.
--depth) clone. This adds the smart
side, in pure PHP with no git program involved.
GitUploadPack, with unit tests. It frames git's
pkt-lines, advertises the repository's refs with the shallow
capability, reads a client's want-and-depth request, walks history
to the requested depth (reporting the shallow boundary), and writes
a fresh packfile of just those objects, reading objects through the
existing GitRepository. Done.GitComponent so a real git clone --depth
against a repository wiki page works: the ref advertisement answers
GET info/refs?service=git-upload-pack, and the packfile
answers POST git-upload-pack. Done./favicon.ico, ResourceController now looks up
the request host's saved appearance and serves that domain's own favicon
from its per-domain resource subfolder, falling back to the global icon
when the domain has none or the file is missing. This matters for pages
that carry no favicon link of their own, such as a served static HTML
file, which would otherwise all show the one shared icon whatever domain
served them. A follow-up guard keeps a found per-domain icon from being
overwritten by the later global-favicon path. Done..icon-button-container
to 1.2rem wide while its height stayed 2rem, so
the call button became a portrait box; the call-control buttons now keep
the square --control-height on every viewport. Second, and the
real cause of the symbol sitting centered on load then jumping to the top a
moment later: setCallState runs on load and calls
setDisplay('video-start', true), whose default display type is
block, so it replaced the button's inline-flex
with block and the flex centering stopped applying. The four
show-calls for video-start and video-end now pass
'inline-flex' as the display type, so the buttons stay flex and
the glyph stays centered. Measured headless: under block the
glyph rides 5px high, under inline-flex it is centered.